package org.bouncycastle.asn1;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.bouncycastle.util.Arrays;

/**
 * Base class for an application specific object
 */
public class DERApplicationSpecific extends ASN1Object {
    private final boolean isConstructed;
    private final int tag;
    private final byte[] octets;

    DERApplicationSpecific(boolean isConstructed, int tag, byte[] octets) {
        this.isConstructed = isConstructed;
        this.tag = tag;
        this.octets = octets;
    }

    public DERApplicationSpecific(int tag, byte[] octets) {
        this(false, tag, octets);
    }

    public DERApplicationSpecific(int tag, DEREncodable object)
            throws IOException {
        this(true, tag, object);
    }

    public DERApplicationSpecific(boolean explicit, int tag, DEREncodable object)
            throws IOException {
        byte[] data = object.getDERObject().getDEREncoded();

        this.isConstructed = explicit;
        this.tag = tag;

        if (explicit) {
            this.octets = data;
        } else {
            int lenBytes = getLengthOfLength(data);
            byte[] tmp = new byte[data.length - lenBytes];
            System.arraycopy(data, lenBytes, tmp, 0, tmp.length);
            this.octets = tmp;
        }
    }

    public DERApplicationSpecific(int tagNo, ASN1EncodableVector vec) {
        this.tag = tagNo;
        this.isConstructed = true;
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();

        for (int i = 0; i != vec.size(); i++) {
            try {
                bOut.write(((ASN1Encodable) vec.get(i)).getEncoded());
            } catch (IOException e) {
                throw new ASN1ParsingException("malformed object: " + e, e);
            }
        }
        this.octets = bOut.toByteArray();
    }

    private int getLengthOfLength(byte[] data) {
        int count = 2; // TODO: assumes only a 1 byte tag number

        while ((data[count - 1] & 0x80) != 0) {
            count++;
        }

        return count;
    }

    public boolean isConstructed() {
        return isConstructed;
    }

    public byte[] getContents() {
        return octets;
    }

    public int getApplicationTag() {
        return tag;
    }

    /**
     * Return the enclosed object assuming explicit tagging.
     * 
     * @return the resulting object
     * @throws IOException
     *             if reconstruction fails.
     */
    public DERObject getObject() throws IOException {
        return new ASN1InputStream(getContents()).readObject();
    }

    /**
     * Return the enclosed object assuming implicit tagging.
     * 
     * @param derTagNo
     *            the type tag that should be applied to the object's contents.
     * @return the resulting object
     * @throws IOException
     *             if reconstruction fails.
     */
    public DERObject getObject(int derTagNo) throws IOException {
        if (derTagNo >= 0x1f) {
            throw new IOException("unsupported tag number");
        }

        byte[] orig = this.getEncoded();
        byte[] tmp = replaceTagNumber(derTagNo, orig);

        if ((orig[0] & DERTags.CONSTRUCTED) != 0) {
            tmp[0] |= DERTags.CONSTRUCTED;
        }

        return new ASN1InputStream(tmp).readObject();
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * org.bouncycastle.asn1.DERObject#encode(org.bouncycastle.asn1.DEROutputStream
     * )
     */
    void encode(DEROutputStream out) throws IOException {
        int classBits = DERTags.APPLICATION;
        if (isConstructed) {
            classBits |= DERTags.CONSTRUCTED;
        }

        out.writeEncoded(classBits, tag, octets);
    }

    boolean asn1Equals(DERObject o) {
        if (!(o instanceof DERApplicationSpecific)) {
            return false;
        }

        DERApplicationSpecific other = (DERApplicationSpecific) o;

        return isConstructed == other.isConstructed && tag == other.tag
                && Arrays.areEqual(octets, other.octets);
    }

    public int hashCode() {
        return (isConstructed ? 1 : 0) ^ tag ^ Arrays.hashCode(octets);
    }

    private byte[] replaceTagNumber(int newTag, byte[] input)
            throws IOException {
        int tagNo = input[0] & 0x1f;
        int index = 1;
        //
        // with tagged object tag number is bottom 5 bits, or stored at the
        // start of the content
        //
        if (tagNo == 0x1f) {
            tagNo = 0;

            int b = input[index++] & 0xff;

            // X.690-0207 8.1.2.4.2
            // "c) bits 7 to 1 of the first subsequent octet shall not all be zero."
            if ((b & 0x7f) == 0) // Note: -1 will pass
            {
                throw new ASN1ParsingException(
                        "corrupted stream - invalid high tag number found");
            }

            while ((b >= 0) && ((b & 0x80) != 0)) {
                tagNo |= (b & 0x7f);
                tagNo <<= 7;
                b = input[index++] & 0xff;
            }

            tagNo |= (b & 0x7f);
        }

        byte[] tmp = new byte[input.length - index + 1];

        System.arraycopy(input, index, tmp, 1, tmp.length - 1);

        tmp[0] = (byte) newTag;

        return tmp;
    }
}
